#pragma once
#include <string>
#include <sstream>
#include "AABB.h"

namespace shermit {
	template<typename Float_t>
	class TriMesh {
	public:
		using vec_t = nv::vec3<Float_t>;
		int3* faces;
		vec_t* vertices;
		int nFaces;
		int nVertices;

		GENERAL TriMesh() : faces(nullptr), vertices(nullptr), nFaces(0), nVertices(0) {}

		void allocateHost(int nv, int nf) {
			deallocateHost();
			vertices = new vec_t[nv];
			faces = new int3[nf];
			nVertices = nv;
			nFaces = nf;
		}

		void deallocateHost() {
			if (nVertices) {
				delete[] vertices;
				nVertices = 0;
			}
			if (nFaces) {
				delete[] faces;
				nFaces = 0;
			}
		}

#if enable_cuda
		void allocateDevice(int nv, int nf) {
			deallocateDevice();
			checkCudaErrors(GPUMalloc(vertices, sizeof(vec_t) * nv));
			checkCudaErrors(GPUMalloc(faces, sizeof(int3) * nf));
			nVertices = nv;
			nFaces = nf;
		}

		void deallocateDevice() {
			if (nVertices) {
				checkCudaErrors(cudaFree(vertices));
				nVertices = 0;
			}
			if (nFaces) {
				checkCudaErrors(cudaFree(faces));
				nFaces = 0;
			}
		}

		void copyToDevice(TriMesh<Float_t>* d_trimesh) {
			d_trimesh->allocateDevice(nVertices, nFaces);
			checkCudaErrors(CPU2GPU(d_trimesh->vertices, vertices, sizeof(vec_t) * nVertices));
			d_trimesh->nVertices = nVertices;
			checkCudaErrors(CPU2GPU(d_trimesh->faces, faces, sizeof(int3) * nFaces));
			d_trimesh->nFaces = nFaces;
		}
#endif
		GENERAL AABB3<Float_t> calcAABB(int iFace) const {
			auto p1 = vertices[faces[iFace].x], p2 = vertices[faces[iFace].y], p3 = vertices[faces[iFace].z];
			AABB3<Float_t> aabb = AABB3<Float_t>();
			aabb.merge(p1);
			aabb.merge(p2);
			aabb.merge(p3);
			return aabb;
		}
	};

	template<typename Float_t>
	void obj2TriMesh(const std::string& filename, TriMesh<Float_t>* h_trimesh) {
		std::ifstream ifs(filename);
		if (!ifs.is_open())
		{
			std::cerr << "Error: cannot open file " << filename << std::endl;
			return;
		}
		std::string line;
		std::string keyWrd;
		Float_t x, y, z;
		std::stringstream stream, lineData, tmp;
		std::vector<int3> h_faces;
		std::vector<nv::vec3<Float_t>> h_vertices;

		// pass 1: read vertices
		while (ifs && !ifs.eof())
		{
			std::getline(ifs, line);
			if (ifs.bad())
			{
				std::cerr << "  Warning! Could not read file properly!\n";
				return;
			}

			// Trim Both leading and trailing spaces
			auto start = line.find_first_not_of(" \t\r\n");
			auto end = line.find_last_not_of(" \t\r\n");
			if ((std::string::npos == start) || (std::string::npos == end))
				line = "";
			else
				line = line.substr(start, end - start + 1);

			if (line.size() == 0 || line[0] == '#' || isspace(line[0]))
			{
				continue;
			}
			stream.str(line);
			stream.clear();
			stream >> keyWrd;
			if (keyWrd == "v")
			{
				stream >> x >> y >> z;
				if (!stream.fail())
				{
					h_vertices.push_back(nv::vec3<Float_t>(x, y, z));
				}
			}
		}

		// reset stream for second pass
		ifs.clear();
		ifs.seekg(0, std::ios::beg);

		int nCurrentPositions = 0;

		// pass 2: read faces
		while (ifs && !ifs.eof())
		{
			std::getline(ifs, line);
			if (ifs.bad())
			{
				std::cerr << "  Warning! Could not read file properly!\n";
				return;
			}

			// Trim Both leading and trailing spaces
			auto start = line.find_first_not_of(" \t\r\n");
			auto end = line.find_last_not_of(" \t\r\n");
			line = ((std::string::npos == start) || (std::string::npos == end)) ? "" : line.substr(start, end - start + 1);

			// comment
			if (line.size() == 0 || line[0] == '#' || isspace(line[0]))
			{
				continue;
			}

			stream.str(line);
			stream.clear();
			stream >> keyWrd;

			// track current number of parsed vertex attributes,
			// to allow for OBJs negative indices
			if (keyWrd == "v")
			{
				++nCurrentPositions;
			}

			// faces
			else if (keyWrd == "f")
			{
				int value;

				// read full line after detecting a face
				std::string faceLine;
				std::getline(stream, faceLine);
				lineData.str(faceLine);
				lineData.clear();

				int3 face;
				int idx = 0;
				// work on the line until nothing left to read
				while (!lineData.eof())
				{
					// read one block from the line ( vertex/texCoord/normal )
					std::string vertex;
					lineData >> vertex;

					//get the component (vertex/texCoord/normal)
					size_t found = vertex.find("/");

					// parts are seperated by '/' So if no '/' found its the last component
					if (found != std::string::npos)
					{
						// read the index value
						tmp.str(vertex.substr(0, found));
						tmp.clear();

						// If we get an empty string this property is undefined in the file
						if (vertex.substr(0, found).empty())
						{
							continue;
						}

						// Read current value
						tmp >> value;
					}
					else
					{
						// last component of the vertex, read it.
						tmp.str(vertex);
						tmp.clear();
						tmp >> value;

						// Nothing to read here ( garbage at end of line )
						if (tmp.fail())
						{
							continue;
						}
					}

					if (value < 0) {
						// Calculation of index :
						// -1 is the last vertex in the list
						// As obj counts from 1 and not zero add +1
						value = nCurrentPositions + value + 1;
					}
					if (idx == 0) face.x = value - 1;
					else if (idx == 1) face.y = value - 1;
					else face.z = value - 1;
					idx++;
					// Obj counts from 1 and not zero .. array counts from zero therefore -1
				}

				h_faces.push_back(face);

			}
		}
		ifs.close();

		h_trimesh->deallocateHost();
		h_trimesh->allocateHost(h_vertices.size(), h_faces.size());
		memcpy(h_trimesh->vertices, &h_vertices[0], sizeof(nv::vec3<Float_t>) * h_trimesh->nVertices);
		memcpy(h_trimesh->faces, &h_faces[0], sizeof(int3) * h_trimesh->nFaces);

		return;
	}
}